<!DOCTYPE html>
<html style="width: 100%; height: 100%;">

<head>
  <meta charset="utf-8">
  <title>Scheduler Test Page</title>
  <style>
    .correct {
      border: solid green 2px;
    }
    .incorrect {
      border: dashed red 2px;
    }
  </style>
</head>

<body>
  <h1>Scheduler Fixture</h1>
  <p>
    This fixture is for manual testing purposes, and the patterns used in
    implementing it should not be used as a model. This is mainly for anyone
    working on making changes to the `schedule` module.
  </p>
  <h2>Tests:</h2>
  <ol>
    <li>
      <button onClick="runTestOne()">Run Test 1</button>
      <p>Calls the callback within the frame when not blocked:</p>
      <div><b>Expected:</b></div>
      <div id="test-1-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If you see the same above and below it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-1"></div>
    </li>
    <li>
      <p>Accepts multiple callbacks and calls within frame when not blocked</p>
      <button onClick="runTestTwo()">Run Test 2</button>
      <div><b>Expected:</b></div>
      <div id="test-2-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If you see the same above and below it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-2"></div>
    </li>
    <li>
      <p>Schedules callbacks in correct order when they use scheduleCallback to schedule themselves</p>
      <button onClick="runTestThree()">Run Test 3</button>
      <div><b>Expected:</b></div>
      <div id="test-3-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If you see the same above and below it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-3"></div>
    </li>
    <li>
      <p>Calls timed out callbacks and then any more pending callbacks, defers others if time runs out</p>
      <button onClick="runTestFour()">Run Test 4</button>
      <div><b>Expected:</b></div>
      <div id="test-4-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If you see the same above and below it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-4"></div>
    </li>
    <li>
      <p>When some callbacks throw errors, still calls them all within the same frame</p>
      <p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
      <button onClick="runTestFive()">Run Test 5</button>
    </li>
    <li>
      <p>When some callbacks throw errors <b> and some also time out</b>, still calls them all within the same frame</p>
      <p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
      <button onClick="runTestSix()">Run Test 6</button>
    </li>
    <li>
      <p>Continues calling callbacks even when user switches away from this tab</p>
      <button onClick="runTestSeven()">Run Test 7</button>
      <div><b>Click the button above, observe the counter, then switch to
          another tab and switch back:</b></div>
      <div id="test-7">
      </div>
      <div> If the counter advanced while you were away from this tab, it's correct.</div>
    </li>
    <li>
      <p>Can pause execution, dump scheduled callbacks, and continue where it left off</p>
      <button onClick="runTestEight()">Run Test 8</button>
      <div><b>Click the button above, press "continue" to finish the test after it pauses:</b></div>
      <button onClick="continueTestEight()">continue</button>
      <div><b>Expected:</b></div>
      <div id="test-8-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If the test didn't progress until you hit "continue" and </div>
      <div> you see the same above and below afterwards it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-8"></div>
    </li>
    <li>
      <p>Can force a specific framerate</p>
      <p><b>IMPORTANT:</b> This test may be flaky if other tests have been run in this js instance. To get a clean test refresh the page before running test 9</p>
      <button onClick="runTestNine()">Run Test 9</button>
      <div><b>Expected:</b></div>
      <div id="test-9-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If you see the same above and below it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-9"></div>
      </div>
    </li>
    <li>
      <p>Runs scheduled JS work for 99% of the frame time when nothing else is using the thread.</p>
      <p><b>NOTE:</b> Try this test both when nothing else is running and when something is using the compositor thread in another visible tab with video or <a href="https://www.shadertoy.com/view/MtffDX">WebGL content</a> (Shift+Click).</p>
      <button onClick="runTestTen()">Run Test 10</button>
      <div><b>Expected:</b></div>
      <div id="test-10-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If you see the same above and below it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-10"></div>
      </div>
    </li>
    <li>
      <p>Runs scheduled JS work more than 95% of the frame time when inserting DOM nodes.</p>
      <p><b>NOTE:</b> Try this test both when nothing else is running and when something is using the compositor thread in another visible tab with video or <a href="https://www.shadertoy.com/view/MtffDX">WebGL content</a> (Shift+Click).</p>
      <button onClick="runTestEleven()">Run Test 11</button>
      <div><b>Expected:</b></div>
      <div id="test-11-expected">
      </div>
      <div> -------------------------------------------------</div>
      <div> If you see the same above and below it's correct.
        <div> -------------------------------------------------</div>
        <div><b>Actual:</b></div>
        <div id="test-11"></div>
      </div>
    </li>
  </ol>
  <script src="../../build/node_modules/react/umd/react.production.min.js"></script>
  <script src="../../build/node_modules/scheduler/umd/scheduler.production.min.js"></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
  <script type="text/babel">
const {
  unstable_scheduleCallback: scheduleCallback,
  unstable_cancelCallback: cancelCallback,
  unstable_now: now,
  unstable_getFirstCallbackNode: getFirstCallbackNode,
  unstable_pauseExecution: pauseExecution,
  unstable_continueExecution: continueExecution,
  unstable_forceFrameRate: forceFrameRate,
  unstable_shouldYield: shouldYield,
  unstable_NormalPriority: NormalPriority,
} = Scheduler;
function displayTestResult(testNumber) {
  const expectationNode = document.getElementById('test-' + testNumber + '-expected');
  const resultNode = document.getElementById('test-' + testNumber);
  resultNode.innerHTML = latestResults[testNumber - 1].join('<br />');
  expectationNode.innerHTML = expectedResults[testNumber - 1].join('<br />');
}
function clearTestResult(testNumber) {
  const resultNode = document.getElementById('test-' + testNumber);
  resultNode.innerHTML = '';
  latestResults[testNumber - 1] = [];
}
function updateTestResult(testNumber, textToAddToResult) {
  latestResults[testNumber - 1].push(textToAddToResult);
};
function checkTestResult(testNumber) {

  let correct = true;
  const expected = expectedResults[testNumber - 1]; // zero indexing
  const result = latestResults[testNumber - 1]; // zero indexing
  if (expected.length !== result.length) {
    correct = false;
  } else {
    for (let i = 0, len = expected.length; i < len; i++) {
      if (expected[i] !== result[i]) {
        correct = false;
        break;
      }
    }
  }
  const currentClass = correct ? 'correct' : 'incorrect';
  const previousClass = correct ? 'incorrect' : 'correct';
  document.getElementById('test-' + testNumber).classList.remove(previousClass);
  document.getElementById('test-' + testNumber).classList.add(currentClass);
}
function logWhenFramesStart(testNumber, cb) {
  requestAnimationFrame(() => {
    updateTestResult(testNumber, 'frame 1 started');
    requestAnimationFrame(() => {
      updateTestResult(testNumber, 'frame 2 started');
      requestAnimationFrame(() => {
        updateTestResult(testNumber, 'frame 3 started... we stop counting now.');
        cb();
      });
    });
  });
}
// push in results when we run the test
const latestResults = [
  // test 1
  [
  ],
  // test 2
  [
  ],
  // test 3
  [
  ],
  // test 4
  [
  ],
  // test 5
  [
  ],
];

const expectedResults = [
  // test 1
  [
    'scheduled Cb1',
    'frame 1 started',
    'cb1 called with argument of false',
    'frame 2 started',
    'frame 3 started... we stop counting now.',
  ],
  // test 2
  [
    'scheduled CbA',
    'scheduled CbB',
    'frame 1 started',
    'cbA called with argument of false',
    'cbB called with argument of false',
    'frame 2 started',
    'frame 3 started... we stop counting now.',
  ],
  // test 3
  [
    'scheduled CbA',
    'scheduled CbB',
    'frame 1 started',
    'scheduled CbA again',
    'cbA0 called with argument of false',
    'cbB called with argument of false',
    'cbA1 called with argument of false',
    'frame 2 started',
    'frame 3 started... we stop counting now.',
  ],
  // test 4
  [
    'scheduled cbA',
    'scheduled cbB',
    'scheduled cbC',
    'scheduled cbD',
    'frame 1 started',
    'cbC called with argument of {"didTimeout":true}',
    'cbA called with argument of false',
    'cbA running and taking some time',
    'frame 2 started',
    'cbB called with argument of false',
    'cbD called with argument of false',
    'frame 3 started... we stop counting now.',
  ],
  // test 5
  [
    // ... TODO
  ],
  [],
  [],
  // Test 8
  [
    'Queue size: 0.',
    'Pausing... press continue to resume.',
    'Queue size: 2.',
    'Finishing...',
    'Done!',
  ],
  // test 9
  [
    'Forcing new frame times...',
    'Using new frame time!',
    'Using new frame time!',
    'Finished!',
  ],
  // test 10
  [
    'Running work for 10 seconds...',
    'Ran scheduled work for >99% of the time.',
  ],
  // test 11
  [
    'Running work for 10 seconds...',
    'Ran scheduled work for >95% of the time.',
  ],
];
function runTestOne() {
  // Test 1
  // Calls the callback with the frame when not blocked
  clearTestResult(1);
  const test1Log = [];
  const cb1Arguments = [];
  const cb1 = (x) => {
    updateTestResult(1, 'cb1 called with argument of ' + JSON.stringify(x));
  }
  scheduleCallback(NormalPriority, cb1);
  updateTestResult(1, 'scheduled Cb1');
  logWhenFramesStart(1, () => {
    displayTestResult(1);
    checkTestResult(1);
  });
};

function runTestTwo() {
  // Test 2
  // accepts multiple callbacks and calls within frame when not blocked
  clearTestResult(2);
  const cbA = (x) => {
    updateTestResult(2, 'cbA called with argument of ' + JSON.stringify(x));
  }
  const cbB = (x) => {
    updateTestResult(2, 'cbB called with argument of ' + JSON.stringify(x));
  }
  scheduleCallback(NormalPriority, cbA);
  updateTestResult(2, 'scheduled CbA');
  scheduleCallback(NormalPriority, cbB);
  updateTestResult(2, 'scheduled CbB');
  logWhenFramesStart(2, () => {
    displayTestResult(2);
    checkTestResult(2);
  });
}

function runTestThree() {
  // Test 3
  // Schedules callbacks in correct order when they use scheduleCallback to schedule themselves
  clearTestResult(3);
  let callbackAIterations = 0;
  const cbA = (x) => {
    if (callbackAIterations < 1) {
      scheduleCallback(NormalPriority, cbA);
      updateTestResult(3, 'scheduled CbA again');
    }
    updateTestResult(3, 'cbA' + callbackAIterations + ' called with argument of ' + JSON.stringify(x));
    callbackAIterations++;
  }
  const cbB = (x) => {
    updateTestResult(3, 'cbB called with argument of ' + JSON.stringify(x));
  }
  scheduleCallback(NormalPriority, cbA);
  updateTestResult(3, 'scheduled CbA');
  scheduleCallback(NormalPriority, cbB);
  updateTestResult(3, 'scheduled CbB');
  logWhenFramesStart(3, () => {
    displayTestResult(3);
    checkTestResult(3);
  });
}

function waitForTimeToPass(timeInMs) {
  const startTime = Date.now();
  const endTime = startTime + timeInMs;
  while (Date.now() < endTime) {
    // wait...
  }
}

function runTestFour() {
  // Test 4
  // Calls timed out callbacks and then any more pending callbacks, defers others if time runs out
  clearTestResult(4);
  const cbA = (x) => {
    updateTestResult(4, 'cbA called with argument of ' + JSON.stringify(x));
    updateTestResult(4, 'cbA running and taking some time');
    waitForTimeToPass(35);
  }
  const cbB = (x) => {
    updateTestResult(4, 'cbB called with argument of ' + JSON.stringify(x));
  }
  const cbC = (x) => {
    updateTestResult(4, 'cbC called with argument of ' + JSON.stringify(x));
  }
  const cbD = (x) => {
    updateTestResult(4, 'cbD called with argument of ' + JSON.stringify(x));
  }
  scheduleCallback(NormalPriority, cbA); // won't time out
  updateTestResult(4, 'scheduled cbA');
  scheduleCallback(NormalPriority, cbB, {timeout: 100}); // times out later
  updateTestResult(4, 'scheduled cbB');
  scheduleCallback(NormalPriority, cbC, {timeout: 1}); // will time out fast
  updateTestResult(4, 'scheduled cbC');
  scheduleCallback(NormalPriority, cbD); // won't time out
  updateTestResult(4, 'scheduled cbD');

  // should have run in order of C, A, B, D

  logWhenFramesStart(4, () => {
    displayTestResult(4);
    checkTestResult(4);
  });

}

// Error handling

function runTestFive() {
  // Test 5
  // When some callbacks throw errors, still calls them all within the same frame
  const cbA = (x) => {
    console.log('cbA called with argument of ' + JSON.stringify(x));
  }
  const cbB = (x) => {
    console.log('cbB called with argument of ' + JSON.stringify(x));
    console.log('cbB is about to throw an error!');
    throw new Error('error B');
  }
  const cbC = (x) => {
    console.log('cbC called with argument of ' + JSON.stringify(x));
  }
  const cbD = (x) => {
    console.log('cbD called with argument of ' + JSON.stringify(x));
    console.log('cbD is about to throw an error!');
    throw new Error('error D');
  }
  const cbE = (x) => {
    console.log('cbE called with argument of ' + JSON.stringify(x));
    console.log('This was the last callback! ------------------');
  }

  console.log('We are aiming to roughly emulate the way ' +
  '`requestAnimationFrame` handles errors from callbacks.');

  console.log('about to run the simulation of what it should look like...:');

  requestAnimationFrame(() => {
    console.log('frame 1 started');
    requestAnimationFrame(() => {
      console.log('frame 2 started');
      requestAnimationFrame(() => {
        console.log('frame 3 started... we stop counting now.');
        console.log('about to wait a moment and start this again but ' +
        'with the scheduler instead of requestAnimationFrame');
        setTimeout(runSchedulerCode, 1000);
      });
    });
  });
  requestAnimationFrame(cbA);
  console.log('scheduled cbA');
  requestAnimationFrame(cbB); // will throw error
  console.log('scheduled cbB');
  requestAnimationFrame(cbC);
  console.log('scheduled cbC');
  requestAnimationFrame(cbD); // will throw error
  console.log('scheduled cbD');
  requestAnimationFrame(cbE);
  console.log('scheduled cbE');


  function runSchedulerCode() {
    console.log('-------------------------------------------------------------');
    console.log('now lets see what it looks like using the scheduler...:');
    requestAnimationFrame(() => {
      console.log('frame 1 started');
      requestAnimationFrame(() => {
        console.log('frame 2 started');
        requestAnimationFrame(() => {
          console.log('frame 3 started... we stop counting now.');
        });
      });
    });
    scheduleCallback(NormalPriority, cbA);
    console.log('scheduled cbA');
    scheduleCallback(NormalPriority, cbB); // will throw error
    console.log('scheduled cbB');
    scheduleCallback(NormalPriority, cbC);
    console.log('scheduled cbC');
    scheduleCallback(NormalPriority, cbD); // will throw error
    console.log('scheduled cbD');
    scheduleCallback(NormalPriority, cbE);
    console.log('scheduled cbE');
  };
}

function runTestSix() {
  // Test 6
  // When some callbacks throw errors, still calls them all within the same frame
  const cbA = (x) => {
    console.log('cbA called with argument of ' + JSON.stringify(x));
    console.log('cbA is about to throw an error!');
    throw new Error('error A');
  }
  const cbB = (x) => {
    console.log('cbB called with argument of ' + JSON.stringify(x));
  }
  const cbC = (x) => {
    console.log('cbC called with argument of ' + JSON.stringify(x));
  }
  const cbD = (x) => {
    console.log('cbD called with argument of ' + JSON.stringify(x));
    console.log('cbD is about to throw an error!');
    throw new Error('error D');
  }
  const cbE = (x) => {
    console.log('cbE called with argument of ' + JSON.stringify(x));
    console.log('This was the last callback! ------------------');
  }

  console.log('We are aiming to roughly emulate the way ' +
  '`requestAnimationFrame` handles errors from callbacks.');

  console.log('about to run the simulation of what it should look like...:');

  requestAnimationFrame(() => {
    console.log('frame 1 started');
    requestAnimationFrame(() => {
      console.log('frame 2 started');
      requestAnimationFrame(() => {
        console.log('frame 3 started... we stop counting now.');
        console.log('about to wait a moment and start this again but ' +
        'with the scheduler instead of requestAnimationFrame');
        setTimeout(runSchedulerCode, 1000);
      });
    });
  });
  requestAnimationFrame(cbC);
  console.log('scheduled cbC first; simulating timing out');
  requestAnimationFrame(cbD); // will throw error
  console.log('scheduled cbD first; simulating timing out');
  requestAnimationFrame(cbE);
  console.log('scheduled cbE first; simulating timing out');
  requestAnimationFrame(cbA);
  console.log('scheduled cbA'); // will throw error
  requestAnimationFrame(cbB);
  console.log('scheduled cbB');


  function runSchedulerCode() {
    console.log('-------------------------------------------------------------');
    console.log('now lets see what it looks like using the scheduler...:');
    requestAnimationFrame(() => {
      console.log('frame 1 started');
      requestAnimationFrame(() => {
        console.log('frame 2 started');
        requestAnimationFrame(() => {
          console.log('frame 3 started... we stop counting now.');
        });
      });
    });
    scheduleCallback(NormalPriority, cbA);
    console.log('scheduled cbA');
    scheduleCallback(NormalPriority, cbB); // will throw error
    console.log('scheduled cbB');
    scheduleCallback(NormalPriority, cbC, {timeout: 1});
    console.log('scheduled cbC');
    scheduleCallback(NormalPriority, cbD, {timeout: 1}); // will throw error
    console.log('scheduled cbD');
    scheduleCallback(NormalPriority, cbE, {timeout: 1});
    console.log('scheduled cbE');
  };
}

function runTestSeven() {
  // Test 7
  // Calls callbacks, continues calling them even when this tab is in the
  // background
  clearTestResult(7);
  let counter = -1;
  function incrementCounterAndScheduleNextCallback() {
    const counterNode = document.getElementById('test-7');
    counter++;
    counterNode.innerHTML = counter;
    waitForTimeToPass(100);
    scheduleCallback(NormalPriority, incrementCounterAndScheduleNextCallback);
  }
  scheduleCallback(NormalPriority, incrementCounterAndScheduleNextCallback);
}

function runTestEight() {
  // Test 8
  // Pauses execution, dumps the queue, and continues execution
  clearTestResult(8);

  function countNodesInStack(firstCallbackNode) {
    var node = firstCallbackNode;
    var count = 0;
    if (node !== null) {
      do {
        count = count + 1;
        node = node.next;
      } while (node !== firstCallbackNode);
    }
    return count;
  }

  scheduleCallback(NormalPriority, () => {

    // size should be 0
    updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`);
    updateTestResult(8, 'Pausing... press continue to resume.');
    pauseExecution();

    scheduleCallback(NormalPriority, function () {
      updateTestResult(8, 'Finishing...');
      displayTestResult(8);
    })
    scheduleCallback(NormalPriority, function () {
      updateTestResult(8, 'Done!');
      displayTestResult(8);
      checkTestResult(8);
    })

    // new size should be 2 now
    updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`);
    displayTestResult(8);
  });
}

function continueTestEight() {
  continueExecution();
}

function runTestNine() {
  clearTestResult(9);
  // We have this to make sure that the thing that goes right after it can get a full frame
  var forceFrameFinish = () => {
    while (!shouldYield()) {
      waitForTimeToPass(1);
    }
    waitForTimeToPass(100);
  }
  scheduleCallback(NormalPriority, forceFrameFinish);
  scheduleCallback(NormalPriority, () => {
    var startTime = now();
    while (!shouldYield()) {}
    var initialFrameTime = now() - startTime;
    var newFrameTime = (initialFrameTime * 2) > 60 ? (initialFrameTime * 2) : 60;
    var newFrameRate = Math.floor(1000/newFrameTime);
    updateTestResult(9, `Forcing new frame times...`);
    displayTestResult(9);
    forceFrameRate(newFrameRate);
    var toSchedule = (again) => {
      var startTime = now();
      while (!shouldYield()) {}
      var frameTime = now() - startTime;
      if (frameTime >= (newFrameTime-8)) {
        updateTestResult(9, `Using new frame time!`);
      } else {
        updateTestResult(9, `Failed to use new frame time. (off by ${newFrameTime - frameTime}ms)`);
      }
      displayTestResult(9);
      if (again) {
        scheduleCallback(NormalPriority, forceFrameFinish);
        scheduleCallback(NormalPriority, () => {toSchedule(false);});
      } else {
        updateTestResult(9, `Finished!`);
        forceFrameRate(0);
        displayTestResult(9);
        checkTestResult(9);
      }
    }
    scheduleCallback(NormalPriority, forceFrameFinish);
    scheduleCallback(NormalPriority, () => {toSchedule(true);});
  });
}

function runTestTen() {
  clearTestResult(10);
  updateTestResult(10, `Running work for 10 seconds...`);
  var testStartTime = now();
  var accumulatedWork = 0
  function loop() {
    var startTime = now();
    while (!shouldYield()) {}
    var endTime = now();
    accumulatedWork += endTime - startTime;
    var runTime = endTime - testStartTime;
    if (runTime > 10000) {
      updateTestResult(10, `Ran scheduled work for ${(100 * accumulatedWork / runTime).toFixed(2)}% of the time.`);
      displayTestResult(10);
      return;
    }
    scheduleCallback(NormalPriority, loop);
  }
  scheduleCallback(NormalPriority, loop);
}

function runTestEleven() {
  clearTestResult(11);
  updateTestResult(11, `Running work for 10 seconds...`);
  var testStartTime = now();
  var lastInsertion = 0;
  var accumulatedWork = 0
  function loop() {
    var startTime = now();
    var timeSinceLastDOMInteraction = startTime - lastInsertion;
    if (timeSinceLastDOMInteraction > 15) {
      lastInsertion = startTime;
      var node = document.createElement('div');
      node.textContent = startTime;
      document.body.appendChild(node);
      document.body.clientHeight; // force layout
    }
    while (!shouldYield()) {}
    var endTime = now();
    accumulatedWork += endTime - startTime;
    var runTime = endTime - testStartTime;
    if (runTime > 10000) {
      updateTestResult(11, `Ran scheduled work for ${(100 * accumulatedWork / runTime).toFixed(2)}% of the time.`);
      displayTestResult(11);
      return;
    }
    scheduleCallback(NormalPriority, loop);
  }
  scheduleCallback(NormalPriority, loop);
}

    </script type="text/babel">
  </body>
</html>
